Skip to content

feat(nodectl): add TONCore Nominator Pool support#39

Merged
Keshoid merged 50 commits intorelease/nodectl/v0.4.0from
feature/sma-4-add-nominator-pool-support
Apr 15, 2026
Merged

feat(nodectl): add TONCore Nominator Pool support#39
Keshoid merged 50 commits intorelease/nodectl/v0.4.0from
feature/sma-4-add-nominator-pool-support

Conversation

@mrnkslv
Copy link
Copy Markdown
Contributor

@mrnkslv mrnkslv commented Mar 31, 2026

Summary

  • Add support for the TON Core Nominator Pool (kind: "toncore_nominator" / PoolConfig::TONCore) alongside existing Single Nominator Pool: deploy-time parameters resolved in contracts, shared elector opcodes with SNP, and CLI/service integration for pool config, deploy, wallet stake resolution, and runtime pool loading.
  • Add TONCore nominator router— a dual-pool extension of TONCore that manages two pools , automatically routes stakes to the free pool , recovers from both, and enables uninterrupted validation across election rounds.

Closes SMA-4

Copilot AI review requested due to automatic review settings March 31, 2026 07:51
@linear
Copy link
Copy Markdown

linear Bot commented Mar 31, 2026

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds support for the TON Nominator Pool (also called TONCore pool or pool-core) to the node control system. Previously, only Single Nominator Pools (SNP) were supported. The implementation includes contract wrappers for pool interaction, message builders for pool operations, configuration support with optional deployment parameters, and CLI commands for pool management.

Changes:

  • Added NominatorPoolWrapperImpl wrapper for interacting with deployed TON Nominator Pools
  • Implemented pool contract state initialization with support for configurable deployment parameters (max nominators, min stakes)
  • Added message builders for pool operations (accept coins, process withdrawals, update validator set, etc.)
  • Extended configuration to support TONCore pools with optional deploy-time parameters
  • Updated deployment commands to support deploying TON Nominator Pools alongside SNP
  • Added configuration commands for managing core pools in the CLI
  • Updated documentation with TONCore pool configuration details

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/node-control/contracts/src/ton_core_nominator/ New module implementing nominator pool wrapper and message builders with comprehensive tests
src/node-control/service/src/runtime_config.rs Added support for opening TONCore pools during runtime configuration
src/node-control/common/src/app_config.rs Extended PoolConfig enum with TONCore variant including optional deployment parameters, with serialization tests
src/node-control/commands/src/commands/nodectl/deploy_cmd.rs Updated pool deployment command to handle both SNP and TONCore pools
src/node-control/commands/src/commands/nodectl/config_wallet_cmd.rs Added TONCore pool support for manual stake operations
src/node-control/commands/src/commands/nodectl/config_pool_cmd.rs Added CLI commands for managing core pools with dedicated configuration options
src/node-control/contracts/src/lib.rs Exported new nominator pool types and functions
src/node-control/README.md Updated documentation for TONCore pool configuration

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/node-control/README.md Outdated
Comment thread src/node-control/common/src/app_config.rs Outdated
mrnkslv and others added 3 commits March 31, 2026 11:02
Made-with: Cursor
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@mrnkslv mrnkslv requested review from a team, ITBear, Keshoid and Lapo4kaKek and removed request for a team March 31, 2026 10:28
Comment thread src/node-control/commands/src/commands/nodectl/config_wallet_cmd.rs Outdated
Comment thread src/node-control/commands/src/commands/nodectl/config_wallet_cmd.rs Outdated
Comment thread src/node-control/commands/src/commands/nodectl/config_pool_cmd.rs Outdated
Comment thread src/node-control/commands/src/commands/nodectl/deploy_cmd.rs Outdated
Comment thread src/node-control/README.md Outdated
Comment thread src/node-control/contracts/src/ton_core_nominator/messages.rs Outdated
Comment thread src/node-control/contracts/src/ton_core_nominator/wrapper.rs
Comment thread src/node-control/common/src/app_config.rs Outdated
Comment thread src/node-control/commands/src/commands/nodectl/config_pool_cmd.rs Outdated
Comment thread src/node-control/service/src/runtime_config.rs Outdated
Comment thread src/node-control/commands/src/commands/nodectl/deploy_cmd.rs Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 29 out of 29 changed files in this pull request and generated 7 comments.

Comments suppressed due to low confidence (1)

src/node-control/elections/src/runner.rs:945

  • recover_stake() uses pool_addr_cache/stake_addr() which resolves to pool.address() (for TonCoreNominatorRouter this is always the first configured pool). If staking was routed to the other TONCore slot (e.g. pool[1] free, pool[0] busy), returned stake will be associated with pool[1] and this code will never query or send recover_stake to that address. This can leave funds unrecovered. For TONCore routers, recovery should iterate inner_pools() (and compute/send recover per pool address) or otherwise track the last staking target per election and use that address for returned-stake queries and recover messages.
    async fn recover_stake(&mut self, node_id: &str) -> anyhow::Result<u64> {
        let elector_addr = self.elector.address().await;
        let node = self.nodes.get_mut(node_id).expect("node not found");
        let elections_addr = node.pool_addr_cache.as_ref().cloned().unwrap_or(elector_addr);

        let amount = self.elector.compute_returned_stake(&node.stake_addr().await).await?;
        node.last_recover_amount = amount;
        if amount > 0 {
            tracing::info!(
                "node [{}] send recover stake: amount={} TON",
                node_id,
                amount as f64 / 1_000_000_000.0
            );
            let fee = RECOVER_FEE + WALLET_COMPUTE_FEE;
            let wallet_balance = node.wallet_balance().await?;
            if wallet_balance < fee {
                anyhow::bail!(
                    "node [{}] low wallet balance: required={} TON, available={} TON",
                    node_id,
                    fee as f64 / 1_000_000_000.0,
                    wallet_balance as f64 / 1_000_000_000.0
                );
            }
            let msg_boc = write_boc(
                &node
                    .wallet
                    .message(
                        elections_addr,
                        RECOVER_FEE,
                        Self::build_recover_stake_payload().await?,
                    )
                    .await?,
            )?;
            node.api.send_boc(&msg_boc).await?;
        }
        Ok(amount)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/node-control/service/src/runtime_config.rs Outdated
Comment on lines +84 to +96
match (&slot.address, &slot.params) {
(Some(addr), _) => addr.parse::<MsgAddressInt>().context("invalid pool address"),
(None, Some(params)) => {
let resolved = resolve_toncore_pool(
validator_addr,
params.validator_share,
None,
Some(params.max_nominators),
Some(params.min_validator_stake),
Some(params.min_nominator_stake),
)?;
Ok(resolved.address)
}
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolve_pool_address_from_config() does not validate TONCore slots where both address and params are present: the (Some(addr), _) arm parses and returns the address without checking it matches the deterministic address derived from params + validator_addr. This can cause CLI actions (deposit/withdraw/deploy targeting) to operate on the wrong contract even though runtime validation (resolve_toncore_pool) would reject it. Consider handling (Some(addr), Some(params)) by calling resolve_toncore_pool(..., Some(addr), ...) (or comparing derived vs explicit) and returning the validated derived address.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validation added

Comment on lines +752 to +772
let pool_name = binding
.pool
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Binding '{}' has no pool configured", self.binding))?;
let pool_cfg = config
.pools
.get(pool_name)
.ok_or_else(|| anyhow::anyhow!("Pool '{}' not found", pool_name))?;

let (wallet_address, wallet_info_data, wallet_secret) =
wallet_info(rpc_client.clone(), wallet_cfg, vault.clone()).await?;

if wallet_info_data.account_state
!= ton_http_api_client::v2::data_models::AccountState::Active
{
anyhow::bail!("Wallet '{}' is {}", binding.wallet, wallet_info_data.account_state);
}

let pool_slot = toncore_pool_slot_from_cli_flags(self.pool_even, self.pool_odd);
let pool_address = resolve_pool_address_from_config(pool_cfg, &wallet_address, pool_slot)?;

Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deposit-validator is documented/implemented as a TONCore-only operation, but this code will happily resolve an SNP pool address and then send the TONCore deposit_validator opcode to it (and attach the deposit amount). This can lead to failed transactions and potentially lost funds (depending on bounce behavior). Add an explicit check that pool_cfg is PoolConfig::TONCore { .. } before resolving/sending.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread src/node-control/commands/src/commands/nodectl/config_pool_cmd.rs
Comment on lines +437 to +463
for (node_id, pool_binding) in self.pools.iter() {
let inner = pool_binding.inner_pools();
let pools = if inner.is_empty() { vec![pool_binding.clone()] } else { inner };
for pool in pools {
let pool_data = match pool.get_pool_data().await {
Ok(d) => d,
Err(e) => {
tracing::warn!(
target: "contracts",
"[{}] get_pool_data error (skipping update_validator_set): pool={} {:#}",
node_id, pool.address().await, e
);
continue;
}
};

tracing::info!(
target: "contracts",
"[{}] pool={} state={} vsc_count={}",
node_id, pool.address().await, pool_data.state, pool_data.validator_set_changes_count
);

if pool.storage_reserve() <= SNP_STORAGE_RESERVE
|| pool_data.state != 2
|| pool_data.validator_set_changes_count >= 2
{
continue;
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ensure_pool_validator_sets_updated() calls pool.get_pool_data() for every pool, then skips SNP pools based on storage_reserve() <= SNP_STORAGE_RESERVE. This means every SNP node will still incur an RPC call each tick even though it can never need update_validator_set. Consider checking storage_reserve() (or pool kind) before calling get_pool_data() to avoid unnecessary load on TON HTTP API and reduce tick latency.

Copilot uses AI. Check for mistakes.
Comment on lines +430 to 435
for (i, (pool_address, state_init)) in deploy_targets.iter().enumerate() {
res.borrow_mut().address = pool_address.to_string();

if pool_info.state == AccountState::Active {
if self.verbose {
println!("The pool '{}' is already deployed", &pool_address);
println!("Update pool info [{}/{}] ...", i + 1, deploy_targets.len());
}
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DeployPoolResult only contains a single address, but deploy pool can deploy multiple TONCore targets in one run (deploy_targets loop). The loop overwrites res.address each iteration, so the JSON output will only reflect the last pool and hide that multiple pools may have been deployed/skipped. Either restrict the command to a single target per invocation (require --pool-even/--pool-odd when dual is configured) or extend the result type to return a list/map of deployed pool addresses and states.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread src/node-control/elections/src/runner.rs Outdated
mrnkslv and others added 7 commits April 14, 2026 10:41
@Keshoid Keshoid changed the title Feature/sma 4 add nominator pool support feat(nodectl): add TONCore Nominator Pool support Apr 14, 2026
@Keshoid Keshoid merged commit 4f95f4e into release/nodectl/v0.4.0 Apr 15, 2026
5 of 6 checks passed
@Keshoid Keshoid deleted the feature/sma-4-add-nominator-pool-support branch April 15, 2026 18:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants